<!DOCTYPE html>
<html lang="zh-CN">
<head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title>
        
            [翻译]Packages as layers, not groups
        
    </title>
    <link rel="icon" href="/img/favicon.png"/>
    
<link rel="stylesheet" href="/css/style.css">

    
<link rel="stylesheet" href="/css/font-awesome.min.css">

    <!-- 
<link rel="stylesheet" href="/css/gitment.css">
 -->
    
<link rel="stylesheet" href="/css/hljs.min.css">

    
<script src="/js/hljs.min.js"></script>
  
    <script>
        var _hmt = _hmt || [];
        (function() {
          var hm = document.createElement("script");
          hm.src = "https://hm.baidu.com/hm.js?8ad9e7d37411eee585b4bd7194806394";
          var s = document.getElementsByTagName("script")[0]; 
          s.parentNode.insertBefore(hm, s);
        })();
    </script>  
    <!-- 
<script src="/js/gitment.browser.js"></script>
   -->
    <link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css">
    <script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script>
<meta name="generator" content="Hexo 5.3.0"><link rel="alternate" href="/atom.xml" title="Nick" type="application/atom+xml">
</head>
<body>
    <header class="header" id="header">
    <h1>
        <a class="title" href="/">
            Nick
        </a>
    </h1>
    <h2>
        <a class="motto">
            天地间的怪事莫过于侦察别人的一些和自己绝不相干的事了
        </a>
    </h2>
    <nav class="navbar">
        <ul class="menu">
            
            
                <li class="menu-item">
                    <a href="/" class="menu-item-link">
                        首页
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a href="/archives" class="menu-item-link">
                        归档
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a target="_blank" rel="noopener" href="https://github.com/StoneQI" class="menu-item-link">
                        Github
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a href="/atom.xml" class="menu-item-link">
                        RSS
                    </a>
                </li>
            
                
            
                
            
                <li class="menu-item">
                    <a href="/category-archive" class="menu-item-link">
                        目录
                    </a>
                </li>
            
                
                
                <li class="menu-item">
                    <a class="menu-item-link search">
                        Search                   
                        <i class="fa fa-long-arrow-right search-icon" aria-hidden="true"></i>
                    </a>
                        <input placeholder="Search..." class="search-input" style="display:none;border:none!important;" onkeydown="onEnter(event)" onkeypress="onEnter(event)"></input>
                </li>
                
        </ul>
    </nav>
</header>
    <main class="main">
        <article class="post">
            <h1>
                <a class="title" href="/2021/01/25/tr-packages-as-layers-not-groups/"> 
                    [翻译]Packages as layers, not groups 
                </a>
            </h1>
            <div class="meta">
                <a class="date"> 
                    <i class="fa fa-calendar" aria-hidden="true"></i>                    
                    2021-01-25   
                </a>
                
                <a class="category">
                    <i class="fa fa-th" aria-hidden="true"></i>  
                </a>
               
                <ul class="category-list"><li class="category-list-item"><a class="category-list-link" href="/categories/go/">go</a></li></ul>
                
                <a class="tag">
                    <i class="fa fa-tags" aria-hidden="true"></i>  
                </a>
                
                    <ul class="tag-list" itemprop="keywords"><li class="tag-list-item"><a class="tag-list-link" href="/tags/go/" rel="tag">go</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E7%BF%BB%E8%AF%91/" rel="tag">翻译</a></li></ul>
            </div>
<div class="toc">
  <ol class="toc-list"><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E7%90%86%E8%A7%A3%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8"><span class="toc-list-text">理解循环引用</span></a></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E6%A0%87%E5%87%86%E5%BA%93%E7%9A%84%E5%81%9A%E6%B3%95"><span class="toc-list-text">标准库的做法</span></a></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E4%B8%AD%E4%BD%BF%E7%94%A8%E5%B1%82"><span class="toc-list-text">应用开发中使用层</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E5%B8%B8%E8%A7%81%E7%9A%84%E5%8C%85%E7%BB%84%E7%BB%87%E6%96%B9%E6%B3%95"><span class="toc-list-text">常见的包组织方法</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E9%9A%94%E7%A6%BB%E6%82%A8%E7%9A%84%E4%B8%9A%E5%8A%A1%E6%A8%A1%E5%9E%8B%EF%BC%88business-domain%EF%BC%89"><span class="toc-list-text">隔离您的业务模型（business domain）</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E9%80%9A%E8%BF%87%E6%8E%A5%E5%8F%A3%E5%88%A0%E9%99%A4%E4%BE%9D%E8%B5%96%E9%A1%B9"><span class="toc-list-text">通过接口删除依赖项</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E9%87%8D%E6%96%B0%E5%8C%85%E8%A3%85packages"><span class="toc-list-text">重新包装packages</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E7%BB%93%E8%AE%BA"><span class="toc-list-text">结论</span></a></li></ol>
</div>
            <div class="content">
                <blockquote>
<p>原文由 Ben Johnson 发表在 <a target="_blank" rel="noopener" href="https://www.gobeyond.dev/">gobeyond</a>，推荐观看原文<a target="_blank" rel="noopener" href="https://www.gobeyond.dev/packages-as-layers/">文章链接</a></p>
</blockquote>
<p>四年前，我写了一篇名为”<a target="_blank" rel="noopener" href="https://www.gobeyond.dev/standard-package-layout/">Standard Package Layout</a>“的文章，试图解决即使是高级Go开发人员最困难的主题之一：package layout(go项目中包的组织)。 但是，大多数开发人员仍在努力用目录的方式组织管理代码。</p>
<p>几乎所有的编程语言都有一种将相关功能分组在一起的机制。 Ruby有gems，Java有packages。 这些语言没有将代码代码分组的标准约定，因为老实说，这并不重要。 一切都取决于个人喜好。</p>
<p>但是，转到Go的开发人员发现他们需要经常关心代码包的组织。 为什么Go软件包与其他语言如此不同？ 这是因为它们不是groups(组)，而是layers(层)。</p>
<a id="more"></a>
<h1 id="理解循环引用"><a href="#理解循环引用" class="headerlink" title="理解循环引用"></a>理解循环引用</h1><p>Go软件包和其他语言之间的主要区别在于Go不允许循环依赖。 程序包A可以依赖程序包B，但是程序包B不能再依赖程序包A。软件包依赖关系只能有一个方向。<br><img src="/img/tr-packages-as-layers-not-groups/2021-01-25-10-51-39.png"></p>
<p>因为这个限制，开发人员在两个程序包中共享通用代码时带来了问题。 通常有两种解决方案：将两个程序包合并为一个程序包或引入第三个程序包。</p>
<p><img src="/img/tr-packages-as-layers-not-groups/2021-01-25-10-54-34.png"></p>
<p>但是，拆分成越来越多的程序包只会使问题继续越扩越大。 最终，相互中将会有大量的软件包，导致结构更加混乱。</p>
<h1 id="标准库的做法"><a href="#标准库的做法" class="headerlink" title="标准库的做法"></a>标准库的做法</h1><p>进行Go编程时，最有用的技巧之一是在需要指导时查看标准库。 没有代码是完美的，但是Go标准库包含了Go语言创建者许多思想的具体实践。</p>
<p>例如，<code>net/http</code>包建立在<code>net</code>包的抽象之上，而<code>net</code>包又建立在其下面的<code>io</code>层的抽象之上。 这种包结构之所以有效，是因为该开发方式中下层的<code>net</code>不需要依赖上层的<code>net/http</code>包，因此避免了循环引用。</p>
<p><img src="/img/tr-packages-as-layers-not-groups/2021-01-25-11-01-49.png"></p>
<p>虽然这在标准库中很好实践，但是很难在实际业务项目开发中进行实践。</p>
<h1 id="应用开发中使用层"><a href="#应用开发中使用层" class="headerlink" title="应用开发中使用层"></a>应用开发中使用层</h1><p>我们将看一个名为<a target="_blank" rel="noopener" href="https://github.com/benbjohnson/wtf">WTF Dial</a>的示例应用程序，因此您可以阅读<a target="_blank" rel="noopener" href="https://www.gobeyond.dev/wtf-dial/">介绍文章</a>以了解更多信息。</p>
<p>在这个应用中，我们有两个逻辑层：</p>
<ol>
<li>一个 SQLite 数据库</li>
<li>一个 HTTP 服务</li>
</ol>
<p>我们创建了两个包 <code>sqlite</code>和<code>http</code>。 许多人任务程序包命名与标准库程序包相同的名称是不规范的。 这是一个正确的批评，您可以将其命名为<code>wtfhttp</code>，但是，我们的<code>http</code>包完全封装了<code>net/http</code>包，因此我们永远不会在同一个文件中同时使用它们。 同时，我发现给每个程序包加前缀既乏味又丑陋，所以我不这样做。</p>
<h2 id="常见的包组织方法"><a href="#常见的包组织方法" class="headerlink" title="常见的包组织方法"></a>常见的包组织方法</h2><p>一种包组织方法的方法是在<code>sqlite</code>包中包含我们的数据类型（例如，<code>User</code>, <code>Dial</code>等）和功能（例如，<code>FindUser()</code>，<code>CreateDial()</code>等）。 我们的<code>http</code>包可能直接依赖于它：</p>
<p><img src="/img/tr-packages-as-layers-not-groups/2021-01-25-11-14-20.png"></p>
<p>这不是一个坏方法，它适用于简单的应用程序。 但是这里存在一些问题。 </p>
<p>首先，我们的数据类型命名为<code>sqlite.User</code>和<code>sqlite.Dial</code>。 这看起来不符合逻辑，因为我们的数据类型属于我们的应用程序，而不是<code>SQLite</code>。</p>
<p>其次，我们的<code>HTTP</code>层现在只能提供来自<code>SQLite</code>的数据。 如果我们需要在两者之间添加缓存层会怎样？ 还是我们如何支持其他数据库，例如<code>Postgres</code>，甚至以<code>JSON</code>形式存储在磁盘上？</p>
<p>最后，由于没有抽象层，因此我们需要为每个<code>HTTP</code>测试运行一个<code>SQLite</code>数据库，而不能使用<code>mock</code>生成数据。 我通常会尽可能地支持端到端测试(类似于系统测试)，但是在高层中引入单元测试也是非常有效和重要的。而且我们不希望每次测试都需要搭建<code>SQLite</code>组件。</p>
<h2 id="隔离您的业务模型（business-domain）"><a href="#隔离您的业务模型（business-domain）" class="headerlink" title="隔离您的业务模型（business domain）"></a>隔离您的业务模型（business domain）</h2><p>第一步需要做的是将我们的业务模型迁移到自己的软件包中。 这也可以称为”application domain(应用程序模型)”。 它是应用程序中专用的数据类型，例如<code>WTF Dial</code> 中的 <code>User</code>, <code>Dial</code>。</p>
<p>在这里，我使用了软件包（<code>wtf</code>）保存我们的业务逻辑。因为它是我们应用程序的名称，并且它是新开发人员打开代码库时首先要看的地方。 其中的数据类型现在更恰当地命名为<code>wtf.User</code>和<code>wtf.Dial</code>。</p>
<p><img src="/img/tr-packages-as-layers-not-groups/2021-01-25-11-36-11.png"></p>
<p>下面是<code>wtf.Dial</code>类型的一个例子:</p>
<pre><code class="go">// dial.go#L14-50
type Dial struct &#123;
    ID int `json:&quot;id&quot;`

    // Owner of the dial. Only the owner may delete the dial.
    UserID int   `json:&quot;userID&quot;`
    User   *User `json:&quot;user&quot;`

    // Human-readable name of the dial.
    Name string `json:&quot;name&quot;`

    // Code used to share the dial with other users.
    // It allows the creation of a shareable link without
    // explicitly inviting users.
    InviteCode string `json:&quot;inviteCode,omitempty&quot;`

    // Aggregate WTF level for the dial.
    Value int `json:&quot;value&quot;`

    // Timestamps for dial creation &amp; last update.
    CreatedAt time.Time `json:&quot;createdAt&quot;`
    UpdatedAt time.Time `json:&quot;updatedAt&quot;`

    // List of associated members and their contributing WTF level.
    // This is only set when returning a single dial.
    Memberships []*DialMembership `json:&quot;memberships,omitempty&quot;`
&#125;
</code></pre>
<p>在此代码中，没有引用任何实现细节，仅是原始数据类型和<code>time.Time</code>。 为了方便起见，添加了<code>JSON</code>标签。</p>
<h2 id="通过接口删除依赖项"><a href="#通过接口删除依赖项" class="headerlink" title="通过接口删除依赖项"></a>通过接口删除依赖项</h2><p>我们的应用程序结构现在看起来更好，但是<code>HTTP</code>依赖<code>SQLite</code>仍然很奇怪。 我们的<code>HTTP</code>服务器希望从基础数据存储中获取数据，它并不特别关心是否为   <code>SQLite</code>。</p>
<p>为了解决这个问题，我们为业务模型域中的服务创建接口。 这些服务通常是创建/读取/更新/删除（CRUD），但可以扩展到其他操作。</p>
<pre><code class="go">// dial.go#L81-L122
// DialService represents a service for managing dials.
type DialService interface &#123;
    // Retrieves a single dial by ID along with associated memberships. Only
    // the dial owner &amp; members can see a dial. Returns ENOTFOUND if dial does
    // not exist or user does not have permission to view it.
    FindDialByID(ctx context.Context, id int) (*Dial, error)

    // Retrieves a list of dials based on a filter. Only returns dials that
    // the user owns or is a member of. Also returns a count of total matching
    // dials which may different from the number of returned dials if the
    // &quot;Limit&quot; field is set.
    FindDials(ctx context.Context, filter DialFilter) ([]*Dial, int, error)

    // Creates a new dial and assigns the current user as the owner.
    // The owner will automatically be added as a member of the new dial.
    CreateDial(ctx context.Context, dial *Dial) error

    // Updates an existing dial by ID. Only the dial owner can update a dial.
    // Returns the new dial state even if there was an error during update.
    //
    // Returns ENOTFOUND if dial does not exist. Returns EUNAUTHORIZED if user
    // is not the dial owner.
    UpdateDial(ctx context.Context, id int, upd DialUpdate) (*Dial, error)

    // Permanently removes a dial by ID. Only the dial owner may delete a dial.
    // Returns ENOTFOUND if dial does not exist. Returns EUNAUTHORIZED if user
    // is not the dial owner.
    DeleteDial(ctx context.Context, id int) error
&#125;
</code></pre>
<p>现在，我们的业务模型包（wtf）不仅指定了数据结构，而且还指定了接口规范，以说明我们的各层如何相互通信。 这使我们的程序包层次结构变得平坦，因此所有程序包现在都依赖于业务模型程序包。 这使我们可以打破包之间的直接依赖关系，并引入替代实现，例如<code>mock</code>。</p>
<p><img src="/img/tr-packages-as-layers-not-groups/2021-01-25-11-46-43.png"></p>
<h2 id="重新包装packages"><a href="#重新包装packages" class="headerlink" title="重新包装packages"></a>重新包装<code>packages</code></h2><p>Repackaging packages</p>
<p>打破软件包之间的依赖关系使我们可以灵活地使用代码。 对于我们的应用程序二进制文件<code>wtfd</code>，我们仍然希望<code>http</code>依赖于<code>sqlite</code>（请参阅<a target="_blank" rel="noopener" href="https://github.com/benbjohnson/wtf/blob/main/cmd/wtfd/main.go#L180-L205"><code>wtf/main.go</code></a>），但是对于测试，我们可以更改<code>http</code>依赖于我们的新<code>mock</code>包（请参阅<a target="_blank" rel="noopener" href="https://github.com/benbjohnson/wtf/blob/main/http/server_test.go#L22-L59"><code>http/server_test.go</code></a>）：</p>
<p><img src="/img/tr-packages-as-layers-not-groups/2021-01-25-11-52-45.png"></p>
<p>对于我们的小型Web应用程序<code>WTF Dial</code>来说，这可能是过度设计，但是随着我们代码库的增长，它变得越来越重要。</p>
<h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>包是Go中强大的工具，但如果将它们视为组而不是图层，则是导致很多困惑。了解了应用程序的逻辑层之后，您可以提取业务域的数据类型和接口协定，并将它们移至应用程序模型包中，以用作所有子包的通用域语言。 随着时间的推移，对于扩展应用程序，定义此模型包至关重要。</p>
<p>如果您对本文有问题，评论，或建议，请访问<a target="_blank" rel="noopener" href="https://github.com/StoneQI/StoneQI.github.io/discussions">GitHub Discussion board</a>,这是一个Github讨论版，使用它可以进行更方便的交流。</p>

            </div>
          
           
            <div class="copyright">
                <div class="name">
                    <a>本文作者:</a>
                    <a>Nick Yang</a>
                </div>
                <div class="link">
                    <a>本文链接:</a>
                    <a class="permalink" href="https://stoneqi.github.io/2021/01/25/tr-packages-as-layers-not-groups/">https://stoneqi.github.io/2021/01/25/tr-packages-as-layers-not-groups/</a>
                </div>
                <div class="license">
                    <a>版权声明:</a>
                    <a>本博客所有文章除特别声明外，均采用许可协议 CC BY-NC-SA 3.0 CN。转载请注明出处！</a>
                </div>
            </div>
            

          


</article>


<div class="tip">
<button class="tip-btn">
    打赏
</button>
<div class="tip-img">
<ul>
    
<li>
    <img src="/img/aipay.jpg"></img>
</li>

 
<li>
    <img src="/img/wechatpay.png"></img>
</li>

</ul>
</div>
</div>

<div class="more">

    <div class="prev">
   <a href="/2021/01/25/backup-note/">  编程备忘录</a>
    </div>

<div></div>

    <div class="next">
    <a href="/2021/01/24/tr-common-crud-design-in-go/"> [翻译]Common CRUD Design in Go </a>
    </div>
    
</div>

<div class="bdsharebuttonbox">
<a href="#" class="bds_weixin fa fa-weixin" data-cmd="weixin" title="分享到微信" style="color:#1cbd8f">
</a>
<a href="#" class="bds_tsina fa fa-weibo" data-cmd="tsina" title="分享到新浪微博" style="color:#ff6363">
</a>
<a href="#" class="bds_twi fa fa-twitter" data-cmd="twi" title="分享到Twitter" style="color:#00A7EB">
</a>
<a href="#" class="bds_fbook fa fa-facebook" data-cmd="fbook" title="分享到Facebook" style="color:#00A7EB">
</a>
</div>
<script>
window._bd_share_config={
    "common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"1","bdSize":"16"},
    "share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='../../../../../static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];
</script>
 
<!-- <div id="comments"></div> -->
<div id="gitalk-container"></div>

<script>
  //   var gitment = new Gitment({
  //   id: "Mon Jan 25 2021 20:16:37 GMT+0800",
  //   owner: "StoneQi",
  //   repo: "stoneqi.github.io",
  //   oauth: {
  //     client_id:"d9f4e8907ec2566c2307",
  //     client_secret: "2390fe6084c9227566e82de1755e3c3de4a13592",
  //   }
  // })
  // gitment.render('comments')

  const gitalk = new Gitalk({
  clientID: "d9f4e8907ec2566c2307",
  clientSecret: "2390fe6084c9227566e82de1755e3c3de4a13592",
  repo: "stoneqi.github.io",      // The repository of store comments,
  owner: "StoneQi",
  admin: ["StoneQi"],
  language: "zh-CN",
  id: "Mon Jan 25 2021 20:16:37 GMT+0800",      // Ensure uniqueness and length less than 50
  distractionFreeMode: false  // Facebook-like distraction free mode
})

gitalk.render('gitalk-container')
</script>
    </main>
    <a class="not-found">not found!</a>
    <div class="search-items">
    </div>
    <a href="#header" id="top" style="display:none">
        <i class="fa fa-sort-asc fa-2x"></i>
    </a>
    <footer class="footer">
    <div class="footer-copyright">©️2017
    <a href="//github.com/Vevlins/toki" class="link" target="_blank">Toki</a>  by Vevlins
    </div>    
</footer>

    
<script src="/js/jquery.js"></script>

    
<script src="/js/toki.js"></script>
  
  <script>hljs.initHighlightingOnLoad();</script>
</body>
</html>